Skip to content

fix(kaos,kdrv): What the end-of-line station needed, and the NFC reader's SPI off DMA - #127

Open
luismiKode wants to merge 6 commits into
developfrom
fix/station-rfid-and-inject
Open

luismiKode wants to merge 6 commits into
developfrom
fix/station-rfid-and-inject

Conversation

@luismiKode

@luismiKode luismiKode commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Six fixes from a night's runs of the end-of-line station (E5) with a DVT in the fixture. All were found there, changed and run again on the same board. Three came out of reading what each row of the station really judges, and the last is a fault in the NFC reader's port that the station only made visible.

fix(kdrv): Decode a close EM4100 tag and give a clone more than one window to verify

With the tag close to the coil the half-bit estimate settled on 16 or 17 samples where the true width at RF/64 and 80 kHz is 20.5, so no window gave a frame. The decoder tries the measured width first and then trial widths of 14 to 24 samples, scaled with the sampling rate; the parity checks still decide what is a frame. The clone's verify reads up to four windows, since the first one is flat while the front end settles.

On the board: test component=rfid failed 5 of 5 with the station's disc in the nest before, and passed 10 of 10 in a row after, three times over tonight (the last on this branch, which is develop with #124's LF carrier change). Host cases added in test_krfid_em4100.c.

fix(kaos): Let the buttons and touch rows judge what the console injects

The station's bench operator (test_dot_final.py --operator-sim) answers the buttons and touch prompts with btn and touch. Both verbs answered ok and neither row heard them: the buttons row judges what the debounce of the pins confirms, the touch row sums samples read from the panel. Every step ran out its time (20 s and 40 s) and a station with nobody at it could not finish a run.

An injected button now reaches the row's judge as a press of the board's own button, and an injected finger is summed and judged at its lift. The injection travels in the event (a ButtonEvt under BUTTONS_INJECT_SIG, a TouchEvt under TOUCH_INJECT_SIG), is posted only while a row is running, and the console names AO_Touch weakly, so minimal still links without it.

One thing to decide rather than discover: with this the buttons and touch rows no longer tell a press injected over the console from a physical one. The station marks a pass run with --operator-sim as BENCH and does not record it, but the firmware accepts the path. If the rows should refuse injections outside a bench session, say so and I will gate it.

On the board: 6 of 6 buttons and 2 of 2 touches in each of 10 passes with --operator-sim, a pass in 26 to 30 s. Four host cases added (test_buttons_ao.c, test_touch_ao.c).

fix(kaos): Let the card's write and read-back decide the sd row at the final station

The sd row runs five phases (identify, read4, read1, a megabyte written, the read-back compared) and its verdict uses the first two, on purpose: a card that will not take a write must not condemn a good slot. At the final station the card is the station's own, a good one, and a board whose slot cannot write passed. Only in AO_Autotest and only for the station's client, a failed write or read-back now fails the row. storage_ao.c is not touched and "a card that refuses to be written to does not condemn the slot" stands everywhere else.

On the board: the five phases pass and the row is PASS in every pass. The failing path is proven on the host only (test_autotest_ao.c): there is no card at the station that refuses a write.

fix(kaos): Ask the station's C5 card for the board's own SDIO width and clock

The station's radio card passed with any width above zero, so a link that had fallen back to one data line or a slower clock left as a good card. It asks for KBSP_C5_SDIO_WIDTH and KBSP_C5_SDIO_KHZ, as the production c5 row always has. Host case added in test_test_dot_ao.c.

On the board: "SDIO 4x40000kHz", the row PASS in every pass.

fix(kaos): Fail the mag row on an axis the self-test does not move

The mag row passed on the part's identity and one conversion. The self-test's shift was logged "a reading, not judged" and step 2 (MAG_TEST_ST, which kqp_signals.h describes as judged and the console already prints) was not published. It is published now, an axis that moves under 150 mG fails the row, and so does a self-test that does not run.

The floor is a choice, not a datasheet figure (the datasheet gives the procedure and no number), and the host case said on purpose that it was not judged for want of one. What it stands on: two boards. The station's DVT in its nest, with the magnetic connector's magnet a few millimetres away, shifts 935 to 1082 mG an axis over 25 readings; another DVT loose on a desk shifts 1003 to 1088 over five. If you would rather keep it unjudged until more boards are measured, or judge it only for the station's client as the sd row does, say so.

fix(kdrv): Take the NFC reader's SPI off DMA, where a read could hold what the chip never sent

At the station the reader now and then stopped seeing its tag for the rest of a boot, with nothing in the log: about one boot in ten. Around it came single windows with no tag (2 % of reads), "the reader would not wake from rest", an identity cached as 0x00 at boot, "amplitude conversion did not complete", and once the nfc task spinning in RFAL until the task watchdog reset the board.

They were one fault. The port handed ESP-IDF buffers in PSRAM with SPI_TRANS_DMA_USE_PSRAM, a path that shares the PSRAM's bus, and now and then a read came back holding something the chip never sent: the IO configuration read 0x00, 0x37, 0x67 or 0xF7 where a trace of every write showed nothing had written it and the chip held 0x07. ESP-IDF raised neither SPI_TRANS_DMA_RX_FAIL nor SPI_TRANS_DMA_TX_FAIL for it. RFAL acts on what it reads, so one bad read was a status bit that never came, an oscillator "not stable", or a FIFO with no tag in it.

The bus is opened without DMA: the peripheral takes a transaction from its own 64-byte buffer, filled and emptied by the CPU, and a longer access goes out in pieces. That is safe because the chip-select is a pin the port drives by hand and RFAL holds it across the whole access. The two PSRAM buffers go; a piece's buffers are 128 bytes of internal RAM (internal RAM static: 409 356 of 417 000).

On the same board, the same loop of nfc_read after a reset: with DMA, 10 corrupt register reads in 100 reads, reads of up to 3.4 s and a blind episode; without DMA, 900 reads over 12 boots with none lost, none corrupt, 0.93 s at most, and 40 of 40 passes of the station's programme with the NFC write on.

The watchdog panic was seen once in about 150 passes and has not come back; that it was this same fault is likely and not proven (the core dump held no data RAM).

Not run with the change, and worth a look before it is trusted: an access of more than 64 bytes, the only case in which a transfer goes out in more than one piece. A Type 2 tag reads 16 bytes and writes 4 at a time, so nothing at the station crosses it; ISO-DEP exchanges, nfc_keys and nfc_dump would, and there is neither a Type 4 tag, a MIFARE Classic card nor an outside reader at the station. Emulation was started and stopped five times and nobody read it from outside.

What was run

  • Host suites: 5557 cases across 8 suites, all passing.
  • tests/target: the full build, nowifi, minimal, minimal_noradio and testdot, each with its RAM and starts check.
  • MISRA through kodeOS's build (cppcheck 2.22): none past the baseline.
  • On a DVT in the E5 fixture, kodeOS develop over this branch, the station's programme with --operator-sim and the NFC and RFID writes on. With this head: 20 of 20 passes with all 29 rows PASS, 180 nfc_read over 3 boots with none lost (0.62 s median, 0.93 s at most), 15 of 15 test component=mag. Earlier in the night, with the commits up to the C5 card's bus: 3 of 3 passes; and with the first two only: 45 passes in which the faults they fix did not come back, one of them losing its NFC row to an nfc_read that saw no tag (the fault the last commit here removes). Not run in any of them: the four pad rows (charge, delivery, GND, data loop).

kodeOS's side (the launcher's icon memory and the station's --discover) is in kodediy/KodeOS, which takes this branch in its sdk pointer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UAYt4sNGU1gkMrcPpLheZ3

luismiKode and others added 2 commits November 13, 2026 19:36
…indow to verify

With a tag close to the coil the signal is strong and the half-bit estimate
settled on 16 or 17 samples where the true width at RF/64 and 80 kHz is
20.5 (the windows that decoded measured 18 to 20), so no window gave a
frame (-2). The host cases model it as an asymmetric envelope. The decoder now tries
the measured width first and then trial widths of 14 to 24 samples, scaled
with the sampling rate; the parity checks still decide what is a frame.

The clone's verify read one window after the write, and the first one is flat
while the front end settles, so a good copy ended in "clone not verified".
It now reads up to four windows.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAYt4seiRecKrhvsophR8u
The final station's bench operator (test_dot_final.py --operator-sim)
answers the buttons and touch prompts with the console's btn and touch
verbs. Both verbs answered ok and neither row ever heard them: the
buttons row judges only what the debounce of the pins confirms, and the
touch row sums only samples read from the panel, so every step ended in
its timeout (20 s and 40 s) and a station with nobody at it could not
finish a run.

An injected button now reaches the row's judge as a press of the board's
own button, and an injected finger is summed and judged at its lift, the
way a tap read from the panel is. Outside a running row nothing changes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAYt4seiRecKrhvsophR8u
luismiKode and others added 2 commits November 13, 2026 21:48
…e final station

The card row writes a megabyte and reads it back against its pattern,
and then leaves both phases out of its verdict: the verdict is the
slot's, so that a counterfeit card that refuses writes does not condemn
a good board. At the final station the card is the station's own and a
good one, and there a megabyte the card will not take, or does not give
back the same, passed the Kode all the same.

AO_Autotest already hears the row's phases. For the final station's
client a failed write or read-back now fails the row whatever the owner
answered; for every other client the owner's verdict stands.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAYt4seiRecKrhvsophR8u
…nd clock

The final station passed the C5's card on any width: a link that had
fallen back to one data line, or to a slower clock, because a line did
not reach the C5, left the station as a good card. The production c5
row has always asked for the board's width and clock; the station's
check now asks for the same.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAYt4seiRecKrhvsophR8u
@luismiKode luismiKode changed the title fix(kaos,kdrv): What the end-of-line station needed: the EM4100 decoder and injected buttons and touch fix(kaos,kdrv): What the end-of-line station needed: EM4100, injected buttons and touch, the sd row, the C5 card's bus Nov 13, 2026
luismiKode and others added 2 commits November 14, 2026 01:31
The magnetometer row passed on the part's identity and one conversion.
The self-test's shift was read and logged as "a reading, not judged",
and the record the console already prints for it (step 2, which the
signals describe as judged on its magnitude) was never published. An
axis that does not move still names itself and still converts, so a
part with a dead axis passed.

The datasheet gives the procedure and no figure, so the floor is not a
spec: 150 mG an axis, where a board that works shows about 1000 (one
board measured: 1065, 986, 995). The row now publishes the step, fails
when an axis stays under the floor, and fails when the self-test cannot
be run at all.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAYt4seiRecKrhvsophR8u
… what the chip never sent

At the end-of-line station the reader now and then stopped seeing its tag
for the rest of a boot, with nothing in the log: about one boot in ten.
Around it came single windows with no tag, "the reader would not wake
from rest", an identity cached as 0x00 at boot, "amplitude conversion did
not complete", and once the nfc task spinning in RFAL until the watchdog.

They were one fault. The port handed ESP-IDF buffers in PSRAM with
SPI_TRANS_DMA_USE_PSRAM, a path that shares the PSRAM's bus, and now and
then a read came back holding something the chip never sent: the IO
configuration read 0x00, 0x37, 0x67 or 0xF7 where a trace of every write
showed nothing had written it and the chip held 0x07. ESP-IDF raised
neither SPI_TRANS_DMA_RX_FAIL nor SPI_TRANS_DMA_TX_FAIL for it. RFAL
acts on what it reads, so one bad read was a status bit that never came,
an oscillator "not stable", or a FIFO with no tag in it.

The bus is opened without DMA. The peripheral then takes a transaction
from its own 64-byte buffer, filled and emptied by the CPU, and a longer
access goes out in pieces. That is safe because the chip-select is a pin
this port drives by hand and RFAL holds it across the whole access. The
two PSRAM buffers and their allocation go; a piece's buffers are 128
bytes of internal RAM.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAYt4sNGU1gkMrcPpLheZ3
@luismiKode luismiKode changed the title fix(kaos,kdrv): What the end-of-line station needed: EM4100, injected buttons and touch, the sd row, the C5 card's bus fix(kaos,kdrv): What the end-of-line station needed, and the NFC reader's SPI off DMA Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants